#If you don't have it yet, you'll need to install ruby-mp3info and rubyzip
#libraries to play with MusicBank, to do that just go to the command line
#and put "gem install ruby-mp3info" and "gem install rubyzip" if RubyGems is
#ok and installed
require "mp3info";
require "zip/zip";
require "builder";
require "tests/music/playlists";
#TODO: Change the renaming system that is incremeanting numbers instead of renaming based on the deep of the copy's number
#TODO: Fix a crazy problem, in the put text at the end if you put obj.to_s instead of writing each file the test will fail!

# Just to add a simple method that returns the length of th song in String format like this "03:41"
class Mp3Info
  
  # Method to return the time round in the "MM:SS" format.
  def time
    mins = length.to_i / 60;
    secs = length.to_i % 60;
    "%02d:%02d" % [mins, secs];
  end
  
end

class MusicBank
  include Playlists;
  attr_reader :songs, :nosongs, :location;
  attr_accessor :finder_options, :rename_format, :run_play, :name;
  
  #Supported extensions for the music files
  EXTENSIONS = [:mp3];
  
  #The phrase usually used to place in XML and playlists
  @@generator = "MusicBank MP3 Manager for Ruby";
  
  #The rename format in case of same file, (%s) is the same name
  @@rename_format = "(%s) ((%n))";
  
  #The comand to be executed when running songs, (%p) is the song or playlist path
  @@run_play = '"C:\Arquivos de programas\Windows Media Player Classic\mplayerc.exe" "(%p)"';
  
  #The level of the finder's precision to find files, options are auto-explaxplained (case[:(in)sensitive], strip[true, false], test[:match, :compare])
  @@finder_options = {:case => :insensitive, :strip => true, :test => :match};
  
  # *location*:: String, the location of the files, absolute path only, with "/" instead of "\".
  # *files*:: Array with String data. If provided will search all the songs names in the directory provided in *location*.
  #
  def initialize(location, files=nil, safe=false)
    @location = sanitize(location);
    Dir.mkdir(@location) rescue SystemCallError; #Create if doesnt exists
    in_dir do
      @location = Dir.pwd;
      #/\.(mp3)$/
      EXTENSIONS.map!{|ext| ext.to_s;}
      @exts = "\.("+EXTENSIONS.join("|")+")$";
      @exts = Regexp.new(@exts);
      @nosongs = Array.new;
      @songs = files || Dir.entries(".");
      raise StandardError, "Can't create a MusicBank object with no files" unless @songs.length > 0;
      if not safe
        @songs = @songs.map do |song|
          if song =~ @exts
            begin
              Mp3Info.new(song)
            rescue StandardError
              @nosongs << song
              song = nil
            end
          else
            song = nil
          end; 
          song;
        end
      end
    end
    @songs.compact!;
    @generator = @@generator;
    @rename_format = @@rename_format;
    @run_play = @@run_play;
    @finder_options = @@finder_options;    
  end
  
  # Play the songs at the *location* directory, create a file named musicbank.m3u inside that directory.
  #
  def self.play(location)
    bank = self.new(location);
    bank.play;
  end
  
  # Rename the songs at *location* and return a new MusicBank object.
  # 
  # *location*:: String, the folder's location to rename the files.
  # *format*:: String with the format to rename songs, see rename instance method.
  #
  def self.rename(location, format)
    bank = self.new(location);
    bank.rename(format);
  end
  
  # Quickly finds the songs and return a new MusicBank object.
  # 
  # *location*:: String, the folder's location to search for the files.
  # *opts*:: The same options in a Hash as used in find method but searchs inside *location*.
  #
  def self.find(location, opts)
    bank = self.new(location);
    bank.find(opts);
  end
  
  # Return a new MusicBank object, can contain some different calls
  #
  #   bank[4]             #=> New object with the song at index number 4
  #   bank[2..4]          #=> New object with the songs between index number 2 and 4
  #   bank[4, 5]          #=> New object with the songs at index number 4 and 5 (not like an array)
  #   bank[0, 2, 14, 18]  #=> Same as above, but songs are at indexes 0, 2, 14 and 18
  #
  def [](*args)
    if args.length > 1 || args[0].kind_of?(Array)
      args = args[0] if args[0].kind_of?(Array);
      songs = args.map{|i| @songs[i]}.flatten;
    elsif args.length == 1
      songs = @songs[args[0]];
    end
    self.class.new(@location, songs, true);
  end
  
  # Return the number (excluding not used songs) of songs in the bank.
  #
  def size
    @songs.length;
  end
  
  alias_method :length, :size;
  
  # Adds an String, Mp3Info or MusicBank to the object.
  #
  def <<(obj)
    if obj.kind_of?(String)
      add_song(obj);
    elsif obj.kind_of?(Mp3Info)
      add_song(obj.filename);
    elsif obj.kind_of?(self.class)
      add_music_bank(obj);
    end
  end
  
  # Add *bank* to the bank as a MusicBank object.
  #
  def add_music_bank(bank)
    bank = bank.copy(@location);
    bank.each{|song, mp3| add_song(song);}
  end
  
  # Add *song* to the bank as String.
  #
  def add_song(song)
    in_dir do
      begin
        Mp3Info.new(song);
        @songs |= [song];
      rescue StandardError
        @nosongs << song;
        song = nil;
      end
    end
  end
  
  # Find songs according to *opts*, a Hash with the conditions to be tested at all the songs, generally they are 
  # ID3 tags (:artist => "Beatles") or the filename (:filename => "Rain Falls Down.mp3"), returns a new MusicBank
  # object with the songs according to the *finder_options* instance variable variable.
  #  
  # Examples of use:
  #
  #   bank.find :artist => "Beatles"                              #=> Return a new MusicBank object containing the songs with artist Beatles
  #   bank.find :artist => "Beatles", :album => "Greatest Hits"   #=> The same above but the songs with album "Greatest Hits"
  #   bank.find :title => "rain"                                  #=> Contains the songs with the name like "rain"
  #
  def find(opts)
    files = (opts[:filename]) ? @songs.map{|msc| msc = nil unless valid_tag?(msc, opts[:filename]); msc;} : find_by_tags(opts);
    files.compact!;
    from_index = opts[:offset] || 0;
    to_index = from_index + (opts[:limit] || (files.length - from_index));
    files = files[from_index...to_index];
    self.class.new(@location, files, true);
  end
  
  # Rename all the songs with a predefined format. *format* is a String containing the predefined format, and some
  # words to be replaced with data, these words are wrapped with "(%", ")" and are the ID3 Tags.
  #  
  # Examples of use:
  #
  #   bank.rename "(%album) - (%artist) - (%title)" #=> Rename all the songs in the bank, new name example "Greatest Hits - Beatles - Yellow Submarine"
  #
  def rename(format="(%artist) - (%title)")
    @songs.each{|song| rename_song(song, format);}
  end
  
  # Renames *song* (see get_song) if exists in the bank with an predefined *format* (see rename for format details), *deep* is for
  # recursive use only, internal.
  #
  def rename_song(song, format, deep=1)
    song = get_song(song);
    new_song = apply_format(song, format.dup);
    #puts "\n43(rename_song): song = #{song.inspect}; new = #{new_song.inspect}; format = #{format.inspect}";
    if @songs.include?(new_song) && song != new_song
      new_song = @rename_format.sub("(%s)", new_song.gsub(@exts, "")).sub("(%n)", deep.to_s);
      #puts "46(if): ....redirect(#{song.inspect}, #{new_song.inspect})"
      rename_song(song, new_song, (deep + 1));
    elsif @songs.include?(song)
      @songs[@songs.index(song)] = new_song;
      #puts @songs.inspect
      in_dir do 
        File.rename(song, new_song);
      end
      #puts "51: Ok, renamed to #{new_song.inspect}";
    end
    new_song;
  end
  
  # Change the *tag* (Symbol or String) of all files with the *value* (String) content.
  #
  def change_tag(tag, value)
    each do |song, mp3|
      mp3.tag[tag.to_s] = value.to_s;
      mp3.close;
    end
  end
  
  alias_method :tag, :change_tag;
  
  # Play the file *song* (see get_song) if provided and inside the bank, or all the files saving an m3u file named 
  # musicbank.m3u in the songs directory.
  #
  def play(song=nil)
    playlist(:m3u, :save => @location + "/musicbank.m3u") unless song;
    command = @location + "/" + ((song) ? get_song(song) : "musicbank.m3u");
    command = @run_play.gsub("(%p)", command);
    #Let the player for another thread instead of waiting it to exit
    Thread.new{system(command);}
  end
  
  # Returns a song as a String or Mp3Info object if *mp3obj* is true, if it's not inside the bank returns nil. 
  # The argument *song_name* can be a String with the filename, a Number with the song's index number in @songs instance
  # variable or a Mp3Info object.
  #
  def get_song(song_name, mp3obj=false)
    song_name = @songs[song_name] if song_name.kind_of?(Fixnum);
    song_name = song_name.filename if song_name.kind_of?(Mp3Info);
    in_dir do
      song = (@songs.include?(song_name)) ? (mp3obj) ? Mp3Info.new(song_name) : song_name : nil;
    end
    song
  end
  
  alias_method :song, :get_song;
  
  # Get the songs of the bank, if *mp3obj* is true, it return an Array of Mp3Info objects instead of an Array with Strings.
  #
  def get_songs(mp3obj=true)
    @songs.map{|song| get_song(song, mp3obj);}
  end
  
  #alias_method :songs, :get_songs;
  
  # Just return a string with the song separated with comma and "and" at the end.
  #
  # Example: Some-Song.mp3, Other-One.mp3 and The-Last.mp3
  #
  def to_s
    str = @songs.first.dup;
    if @songs.length > 2
      @songs[1..(@songs.length-2)].each do |song|
        str << ", " + song;
      end
    end
    if @songs.length > 1
      str << " and " + @songs.last;
    end
    str;
  end
  
  # Returns string with formated XML file, with some head information such as generator, created_at and the number of songs.
  # *opts*:: Hash with some options such as :*save* (works like playlist) and :*indent*, the number of spaces to use as identation.
  #
  def to_xml(opts={})
    xmlfile = "";
    xml = Builder::XmlMarkup.new(:target => xmlfile, :indent => (opts[:indent] || 4));
    xml.instruct!
    xml.musicbank do
      xml.head do
        xml.generator(@generator);
        xml.songs(@songs.length.to_s);
        xml.created_at(DateTime.now.strftime("%F %T"));
      end
      xml.body do
        each do |song, mp3|
          xml.music(:length => mp3.length.to_s)  do
            xml.path(get_path(opts) + song);
            mp3.tag.each{|tag, value| xml.tag!(tag.sub("_s", "").to_sym, value);}
          end
        end
      end
    end
    write_file(opts[:save], xmlfile) if opts[:save];
    xmlfile;
  end
  
  # Iterates along the bank's songs, receives a block and call it with 2 arguments, first the String *song*, and  second the 
  # *mp3obj*, Mp3Info object of that song.
  #
  def each
    in_dir do
      for song in @songs
        mp3obj = Mp3Info.new(song);
        yield(song, mp3obj);
      end
    end
  end
  
  # Delete the actually bank.
  #
  def delete
    use_songs(@location){|song, mp3| File.delete(song);}
    true;
  end
   
  alias_method :destroy, :delete;
    
  # Copy the current songs into the new directory *dir*. Create if doesn't exist, but if exist just add the files. Then return a 
  # new MusicBank object containing the files that have been moved and the new location. Note that if the target directory 
  # have more songs inside, the object will contain just the moved ones.
  #
  def copy(dir)
    dir = sanitize(dir, true);
    use_songs(dir){|song, mp3| File.copy(song, dir);}
  end
  
  # Move the current songs into the new directory *dir*. Create if doesn't exist, but if exist just add the files. Then return a 
  # new MusicBank object containing the files that have been moved and the new location. Note that if the target directory 
  # have more songs inside, the object will contain just the moved ones.
  #  
  def move(dir)
    dir = sanitize(dir, true);
    use_songs(dir){|song, mp3| File.move(song, dir);}
  end

  # Put all the songs inside the bank in a .zip file. The *file* (String) argument can be the filename, this case will save 
  # in the songs' directory, or the path plus the filename. If the .zip file already exists it just add content, and if 
  # the songs already exists it will do what *replace* says, *true* to replace and *false* to not replace.
  #
  def zip(file, replace=true)
    in_dir do
      create_dir(file) if file.match(/(\/|\\)/)
      Zip::ZipFile.open(file, true) do |zip|
        each{|song, mp3| zip.add(song, @location+"/"+song){replace};}
        zip;
      end
    end
  end
  
  # Create a playlist according to the format and returns a +String+ with the content. 
  # Generally you can provide a Hash data of options, they are diferent from a format 
  # to another.
  #
  # *format*:: A Symbol, the format that the playlist will be written, some of them are .wpl, .m3u and .pls
  # *args*::
  #   Actually all playlist builders just support one Hash with some options. Each one has some different options, 
  #   by default to all of them is the :*save* option that contains the path the playlist will be exported.
  #   * If is provided just a filename without directory (Ex. _list.wpl_) it saves the playlist with \
  #     relative paths at the same directory as the songs are stored in.
  #   * If is a complete path it saves in that path and with absolute paths of the songs' directories.
  #
  # Examples of use
  #
  #   bank.playlist :wpl, :save => "D:/list.wpl", :title => "Robbie"     #=> String with the wpl, stores in "D:"
  #   bank.playlist :wpl, :save => "myplaylist.wpl"                      #=> String with the wpl, stores in the songs' directory
  #   bank.playlist :wpl                                                 #=> String with the wpl, doesn't store
  #   bank.playlist :wpl, :title => "My Playlist"                        #=> String with the wpl, doesn't store
  #
  def playlist(format, *args)
    format = "playlist_#{format.to_s}".to_sym;
    if self.respond_to?(format, true)
      self.send(format, *args);
    else
      raise NoMethodError, "MusicBank do not suport #{format.to_s.sub(/^playlist_/, "")} playlist";
    end
  end
  
  alias_method :method_missing2, :method_missing; #:nodoc:
  
  # To use a tag with the prefix "find_by_" with the method find, to multiple tags
  # keep using find method.
  #
  # Examples of use
  #
  #   find_by_artist "Beatles"                              #=> Works
  #   find :artist => "Beatles"                             #=> Works, same as above
  #   find :artist => "Beatles", :album => "Greatest Hits"  #=> Works
  #   find_by_artist "Beatles", :album => "Greatest Hits"   #=> Doesn't work, return all songs with artist like "Beatles",
  #                                                         #   but not all have the album like "Greatest Hits"
  #
  def method_missing(method, *args, &block)
    if method.to_s =~ /^find_by_(.*)/
      find($1.to_sym => args[0]);
    else
      method_missing2(method, *args, &block);
    end
  end
  
  private
  # A helper to iterate the songs and create a directory if doesn't exist. Return a MuscBank object with the new 
  # dir and actual songs
  #
  def use_songs(dir, &block)
    create_dir(dir);
    each(&block);
    in_dir{self.class.new(dir, @songs, true);}
  end
  
  def sanitize(location, return_dir=false)
    location = location.gsub(/\\/, "/").gsub(/(^\/|\/$)/, "")
    location = location.sub(/\/[^\/]*\..*$/, "") if return_dir;
    location;
  end
  
  def in_dir(dir=@location)
    old_dir = Dir.pwd;
    Dir.chdir(dir) unless old_dir == dir;
    res = yield(old_dir);
    Dir.chdir(old_dir) unless old_dir == dir;
    res;
  end
  
  # Create the *dir* if doesn't exist
  #
  def create_dir(dir)
    in_dir do
      unless File.directory?(dir)
        dirs = sanitize(dir, true).split("/");
        dirs.each do |dir|
          begin Dir.chdir(dir+"/")
          rescue StandardError
            Dir.mkdir(dir);
            redo;
          end
        end
        Dir.chdir "../" * dirs.size;
      end
    end
  end
  
  def apply_format(filename, format)
    format = format.dup; #Problens with the gsub! below, so duplicate is fastest
    mp3song = Mp3Info.new(filename);
    mp3song.tag.each{|tag, value| format.gsub!("(%#{tag})", value) if tag.kind_of?(String) && value.kind_of?(String);}
    format + ".mp3";
  end
  
  def find_by_tags(conditions)
    tags = {};
    sample_song = get_song(0, true);
    conditions.each{|tag, value| tags[tag.to_sym] = value if sample_song.tag[tag.to_s];}
    songs = @songs.map{|song| song if valid_file?(song, tags);}
    songs.compact;
  end
  
  def valid_file?(filename, tags)
    file = Mp3Info.new(filename); valid = true;
    tags.each{|tag, value| valid = false unless valid_tag?(file.tag[tag.to_s], value);}
    valid;
  end
  
  def valid_tag?(tag, value)
    opts = @finder_options; valid = false;
    tag, value = tag.downcase, value.downcase if opts[:case] == :insensitive;
    tag, value = tag.strip, value.strip if opts[:strip];
    if opts[:test] == :match
      valid = true if tag.match(value);
    else #Its a compare test (:compare)
      valid = true if tag == value;
    end
    valid;
  end
  
  # Get the path, if its without directory (Ex. "list.wpl"), return an
  # relative path "./" otherwise return the location itself.
  def get_path(opts={})
    path = @location + "/";
    #We could go ternary way but it would be complex to understand,
    #if the hash passed has the :save value and if <del>its equals to
    #the location or</del><ins>is</ins> just a file than we return 
    #a relative path "./"
    if opts[:save]
      #Not anymore if its an equal location
      #path = "./" if opts[:save].sub(/\/[^\/]+$/, "") == @location || !opts[:save].include?("/");
      path = "./" if !opts[:save].include?("/");
    end
    path;
  end
  
  # A simple helper to write files
  def write_file(file, content="")
    in_dir do
      File.open(file, "w") do |file|
        file << content;
        file;
      end
    end
  end
  
end
